04. Scope

If you took Intro to Javascript, you learned about block scope vs. function scope. These determine where a variable can be seen in some code. Computer scientists call this lexical scope.

However, there also exists another kind of scope called runtime scope. When a function is run, it creates a new runtime scope. This scope represents the context of the function, or more specifically, the set of variables available for the function to use.

So what exactly does a function have access to?

OOJS L2 32 - Scope Intro

Scope

A function's runtime scope describes the variables available for use inside a given function. The code inside a function has access to:

  1. The function's arguments.
  2. Local variables declared within the function.
  3. Variables from its parent function's scope.
  4. Global variables.

Check out the following image that highlights a function's scope, then we'll take a look at a live example.

_The nested `child()` function has access to all `a`, `b`, and `c` variables. That is, these variables are in the `child()` function's scope._

The nested child() function has access to all a, b, and c variables. That is, these variables are in the child() function's scope.

L2 - 34 - Scope Demo

Video Recap

In the previous video, the introduceMyself() function contains a nested introduce() function. While introduce() does not take in any arguments, nor are there any local variables declared within it -- variables in both the aforementioned settings are indeed in introduce()'s scope.

introduce() does use the global variable myName, however, as well as the you variable contained in its parent function, introduceMyself() (where introduce() was defined). Both are highlighted below:

const myName = 'Andrew';
// Global variable

function introduceMyself() {

  const you = 'student';
  // Variable declared where introduce() is defined
  // (i.e., within introduce()'s parent function, introduceMyself())

  function introduce() {
    console.log(`Hello, ${you}, I'm ${myName}!`);
  }

  return introduce();
}

Consider the following:

const num1 = 5;

function functionOne() {
  const num2 = 10;

  function functionTwo(num3) {
    const num4 = 35;

    return num1 + num2 + num3 + num4;
  }

  return functionTwo(0);
}

Which variables does functionTwo() have access to? Select all that apply:

SOLUTION:
  • `num1`
  • `num2`
  • `num3`
  • `num4`

JavaScript is Function-Scoped

You may be wondering why scope is so heavily associated with functions in JavaScript. Especially if you've had past experience in another programming language, this might seem a bit unusual (e.g., blocks in Ruby have their own scope)!

This is all because variables in JavaScript are traditionally defined in the scope of a function, rather than in the scope of a block. Since entering a function will change scope, any variables defined inside that function are not available outside of that function. On the other hand, if there are any variables defined inside a block (e.g., within an if statement), those variables are available outside of that block.

Let's see an example of how function-scoping in JavaScript works:

var globalNumber = 5;

function globalIncrementer() {
  const localNumber = 10;

  globalNumber += 1;
  return globalNumber;
}

In the example above, globalNumber is outside the function; it is a global variable that the globalIncrementer() function has access to. globalIncrementer() simply has a local variable (localNumber) declared within it, then increments globalNumber by 1 before returning the updated value of globalNumber itself.

After calling the function a few times, we see that the value of globalNumber has indeed increased each time:

console.log(globalIncrementer());
// 6

console.log(globalIncrementer());
// 7

console.log(globalIncrementer());
// 8

However, when attempting to access localNumber outside of the function, we see a error:

console.log(localNumber);

// ReferenceError: localNumber is not defined

Because JavaScript is function-scoped, functions have access to all its own variables as well as all the global variables outside of it. For more details on block scoping, check out Further Research at the end of this page.

💡 Block-Scoping 💡

ES6 syntax allows for additional scope while declaring variables with the let and const keywords. These keywords are used to declare block-scoped variables in JavaScript, and largely replace the need for var.

We've used them throughout this course, but for a closer look, check out our course: ES6 - JavaScript Improved. Via MDN:

Scope Chain

Whenever your code attempts to access a variable during a function call, the JavaScript interpreter will always start off by looking within its own local variables. If the variable isn't found, the search will continue looking up what is called the scope chain. Let's take a look at an example:

function one() {
  two();
  function two() {
    three();
    function three() {
      // function three's code here
    }
  }
}

one();

In the above example, when one() is called, all the other nested functions will be called as well (all the way to three()).

You can visualize the scope chain moving outwards starting at the innermost level: from three(), to two(), to one(), and finally to window (i.e., the global/window object). This way, the function three() will not only have access to the variables and functions "above" it (i.e., those of two() and one()) -- three() will also have access to any global variables defined outside one().

Let's now revisit the image from the beginning of this section, and visualize the entire process:

_When resolving a variable, the JavaScript engine begins by looking at the nested child function's locally-defined variables. If found, then the value is retrieved; if not, the JavaScript engine continues to looking outward until the variable is resolved. If the JavaScript engine reaches the global scope and is still unable to resolve the variable, the variable is undefined._

When resolving a variable, the JavaScript engine begins by looking at the nested child function's locally-defined variables. If found, then the value is retrieved; if not, the JavaScript engine continues to looking outward until the variable is resolved. If the JavaScript engine reaches the global scope and is still unable to resolve the variable, the variable is undefined.

💡 The Global (window) Object💡

Recall that when JavaScript applications run inside a host environment (e.g., a browser), the host provides a window object, otherwise known as the global object. Any global variables declared are accessed as properties of this object, which represents the outermost level of the scope chain.

For a refresher, feel free to check out Beware of Globals in Lesson 1.

Variable Shadowing

What happens when you create a variable with the same name as another variable somewhere in the scope chain?

JavaScript won't throw an error or otherwise prevent you from creating that extra variable. In fact, the variable with local scope will just temporarily "shadow" the variable in the outer scope. This is called variable shadowing. Consider the following example:

const symbol = 'Â¥';

function displayPrice(price) {
  const symbol = '$';
  console.log(symbol + price);
}

displayPrice('80');
// '$80'

In the above snippet, note that symbol is declared in two places:

  1. Outside the displayPrice() function, as a global variable.
  2. Inside the displayPrice() function, as a local variable.

After invoking displayPrice() and passing it an argument of '80', the function outputs '$80' to the console.

How does the JavaScript interpreter know which value of symbol to use? Well, since the variable pointing to '$' is declared inside a function (i.e., the "inner" scope), it will override any variables of the same name that belong in an outer scope -- such as the global variable pointing to 'Â¥'. As a result, '$80' is displayed rather than 'Â¥80'.

All in all, if there are any naming overlaps between variables in different contexts, they are all resolved by moving through the scope chain from inner to outer scopes (i.e., local all the way to global). This way, any local variables that have the same name take precedence over those with a wider scope.

What will the console display when myFunction() is called?

let n = 2;

function myFunction() {
  let n = 8;
  console.log(n);
}

myFunction();
// ???
SOLUTION: `8`

QUIZ QUESTION::

When searching for variables along the scope chain, in what order will the JavaScript interpreter search?

ANSWER CHOICES:



Order

Location

Local variables

Parent function's parent function's variables

Parent function's variables

Global variables

SOLUTION:

Order

Location

Local variables

Parent function's parent function's variables

Parent function's variables

Global variables

When the following code runs, what is the output of the first, second, and third logs to the console (respectively)?

let n = 8;

function functionOne() {
  let n = 9;

  function functionTwo() {
    let n = 10;
    console.log(n);  // First log
  }

  functionTwo();

  console.log(n);  // Second log
}

functionOne();

console.log(n);  // Third log
SOLUTION: `10`, `9`, `8`

Summary

When a function is run, it creates its own scope. A function's scope is the set of variables available for use within that function. The scope of a function includes:

  1. The function's arguments.
  2. Local variables declared within the function.
  3. Variables from its parent function's scope.
  4. Global variables.

Variables in JavaScript are also function-scoped. This means that any variables defined inside a function are not available for use outside the function, though any variables defined within blocks (e.g. if or for) are available outside that block.

When it comes to accessing variables, the JavaScript engine will traverse the scope chain, first looking at the innermost level (e.g., a function's local variables), then to outer scopes, eventually reaching the global scope if necessary.

In this section, we've seen quite a few examples of a nested function being able to access variables declared in its parent function's scope (i.e., in the scope in which that function was nested). These functions, combined with the lexical environment it which it was declared, actually have a very particular name: closure. Closures are very closely related to scope in JavaScript, and lead to some powerful and useful applications. We'll take a look at closures in detail next!

Further Research